/**
 * Copyright (C) 2008 GeoScheduler Team, as stated on <http://www.geoscheduler.org/authors>.
 * 
 * This file is part of GeoScheduler.
 * 
 * GeoScheduler is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GeoScheduler is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with GeoScheduler. If not, see <http://www.gnu.org/licenses/>.
 */

package org.geoscheduler.commons.tools;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * 
 * <h2>PropertyDescriptor</h2>
 * <p>
 * Object property accessing tool.
 * </p>
 * <p>
 * Class aims to be simple replacement for java.beans.BeanDescriptor. JDK tool
 * is not used to be portable to Android platform, that does not support
 * java.beans package.
 * </p>
 * <p>
 * Thought {@link PropertyDescriptor} has public constructor new instances
 * should be created using {@link #valueOf(Class, Method)} static factory
 * method.
 * </p>
 * <p>
 * WARNING: API of this class may change in future releases.
 * </p>
 * 
 * @author LKo (Lukas.Kolisko@Geoscheduler.Org)
 */
public class PropertyDescriptor<T> {

	private final Class<T> type;
	private final String name;
	private final Method setter;
	private final Method getter;

	private static Map<Class<?>, Annotation> annotCache = new WeakHashMap<Class<?>, Annotation>();

	public PropertyDescriptor(Method getter, Method setter, String name, Class<T> type) {
		super();
		this.getter = getter;
		this.setter = setter;
		this.name = name;
		this.type = type;
	}

	/**
	 * @param instance
	 * @param value
	 *            of property for <code>instance</code>
	 */
	public void setValue(Object instance, T value) {
		try {
			setter.invoke(instance, value);
		} catch (Exception ex) {
			throw new IllegalStateException("failed setter [" + this + "] on " + instance + " value " + value, ex);
		}
	}

	/**
	 * @param instance
	 * @return value of property for <code>instance</code>
	 */
	public T getValue(Object instance) {
		try {
			return type.cast(getter.invoke(instance));
		} catch (Exception ex) {
			throw new IllegalStateException("failed getter [" + this + "] on " + instance, ex);
		}
	}

	/**
	 * @return common name of property
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * @return type of property
	 */
	public Class<T> getType() {
		return type;
	}

	/** @return getter declaring class */
	// FIXME [LKo] may be inconsistent when getter and setter defined on
	// different positions
	// in type hierarchy
	public Class<?> getOwnerType() {
		return getter == null ? null : getter.getDeclaringClass();
	}

	/** @return property getter method */
	public Method getGetterMethod() {
		return this.getter;
	}

	/** @return property setter method */
	public Method getSetterMethod() {
		return this.setter;
	}

	/**
	 * Retrieve annotation on getter or setter of wrapped property.
	 * 
	 * @param annotType
	 *            class type of annotation
	 * @return {@link Annotation} instance of <code>null</code> if not found.
	 */
	public <V extends Annotation> V getAnnotation(Class<V> annotType) {
		// FIXME [LKo] very slow
		synchronized (annotCache) {
			V ret = null;
			if (!annotCache.containsKey(annotType)) {
				ret = getAnnotation(annotType, getter);
				if (ret == null) {
					ret = getAnnotation(annotType, setter);
				}
				annotCache.put(annotType, ret);
			} else {
				ret = (V) annotCache.get(annotType);
			}
			return ret;
		}
	}

	private <V extends Annotation> V getAnnotation(Class<V> type, Method method) {
		return method != null && method.isAnnotationPresent(type) ? method.getAnnotation(type) : null;
	}

	/**
	 * Factory method creates new instance of {@link PropertyDescriptor}.
	 * Factory searches for property counterpart method for provided
	 * <code>method</code> object. If no such method is found for specified
	 * <code>type</code> found, then {@link IllegalArgumentException} is
	 * thrown.
	 * 
	 * @param type
	 *            owner type of property
	 * @param method
	 *            setter/getter method object
	 * @return new instance of property descriptor
	 * @throws IllegalArgumentException
	 *             when no setter/getter counterpart for <code>method</code>
	 *             argument found.
	 */
	public static PropertyDescriptor<Object> valueOf(Class<?> type, Method method) throws IllegalArgumentException {
		Method setter = null;
		Method getter = null;
		String name = propertyName(method.getName());
		String fullNameSuffix = String.valueOf(name.charAt(0)).toUpperCase() + name.substring(1);

		if (method.getName().startsWith("set")) {
			setter = method;
			if ((getter = tryGetMethod(type, "get" + fullNameSuffix)) == null) {
				getter = tryGetMethod(type, "is" + fullNameSuffix);
				if (getter == null) {
					throw new IllegalArgumentException(String.format("failed to find getter for type %s and method %s",
							type.getName(), method.getName()));
				}
			}
		} else {
			getter = method;
			if ((setter = tryGetMethod(type, "set" + fullNameSuffix, method.getReturnType())) == null) {
				throw new IllegalArgumentException(String.format("failed to find setter for type %s and method %s",
						type.getName(), method.getName()));

			}
		}
		return new PropertyDescriptor<Object>(getter, setter, name, (Class<Object>) getter.getReturnType());
	}

	/**
	 * @return <code>true</code> if annotation of <code>annotType</code>
	 *         type found on getter or setter of property. Introspection process
	 *         is optimized using cache, but method is now not as fact as it
	 *         should be.
	 */
	public boolean isAnnotationPresent(Class<? extends Annotation> annotType) {
		return getAnnotation(annotType) != null;
	}

	/**
	 * @see #tryGetMethod(Class, String, Class...)
	 */
	public static Method tryGetMethod(Class<?> type, String name) {
		try {
			return type.getMethod(name);
		} catch (SecurityException e) {
			return null;
		} catch (NoSuchMethodException e) {
			return null;
		}
	}

	/**
	 * Static tool method to retrieve method object for specified
	 * <code>type</code> and <code>name</code>. Argument types
	 * <code>argType</code> can be used as predicate for searching.
	 * 
	 * @param type
	 * @param name -
	 *            full name of method
	 * @param argType -
	 *            argument types of method on order as defined in method
	 * @return {@link Method} object instance if found, <code>null</code>
	 *         otherwise.
	 */
	public static Method tryGetMethod(Class<?> type, String name, Class<?>... argType) {
		try {
			return type.getMethod(name, argType);
		} catch (SecurityException e) {
			return null;
		} catch (NoSuchMethodException e) {
			return null;
		}
	}

	/**
	 * Extract base property name
	 * 
	 * @param name
	 *            name with JavaBeans setter or getter prefix
	 * @return base JavaBean property name without prefix
	 */
	public static String propertyName(String name) {
		String ret = null;
		if (name.startsWith("is")) {
			ret = name.substring(2);
		}
		if (name.startsWith("get") || name.startsWith("set")) {
			ret = name.substring(3);
		}
		if (StringTools.isEmpty(ret)) {
			throw new IllegalArgumentException("not a property method name: " + name);
		}
		return String.valueOf(ret.charAt(0)).toLowerCase() + (ret.length() > 0 ? ret.substring(1) : "");

	}

	@Override
	public String toString() {
		return String.format("PropertyDescriptor [ownerType:%s,type:%s,name:%s]", getOwnerType(), type, name);
	}

}
